# Lianstar-RS Board(UI) by clonne/2023
from typing import *
from pathlib import Path
from datetime import datetime
from traceback import print_exc
from queue import Queue
import ramda as R
import protocol,game,ux,lang,conf,logger

class Const:
    ENGINE_DIR = Path("engine/")
    RS_PATH = Path("lianstar-rs.exe")
    DEFAULT_LANG_ID = "zh_CN"
    FPS = 30

class Player:
    DEFAULT_ENGINE_ID = -1
    def __init__(me, init_piece:int, second_step:int, second_all:int, engine_id:int=-1) -> None:
        me.__piece = init_piece
        me.__second_step = second_step
        me.__second_all = second_all
        me.__use_ai = engine_id > Player.DEFAULT_ENGINE_ID
        me.__engine_id = engine_id

    def name(me) -> str: return  lang.piece_name(me.__piece)

    def piece(me) -> int: return me.__piece

    def second_all(me) -> int: return me.__second_all

    def second_step(me) -> int: return max(0, me.__second_step)

    def has_overtime(me) -> bool: return (me.__second_step < 0)

    def on_second(me) -> Self:
        if me.__second_step >= 0:
            me.__second_all = max(0, me.__second_all - 1)
            me.__second_step -= 1
        return me

    def set_seconds(me, step:int, all:int) -> Self:
        me.__second_step = max(0, step)
        me.__second_all = max(0, all)
        return me

    def engine_id(me) -> int: return me.__engine_id

    def use_ai(me) -> bool: return me.__use_ai

    def set_piece(me, piece_to:int) -> Self:
        me.__piece = piece_to
        return me

    def enable_ai(me, on:bool) -> Self:
        me.__use_ai = on
        return me

    def set_engine(me, engine_id:int) -> Self:
        me.__engine_id = engine_id
        return me

class EngineConf:
    def __init__(me) -> None:
        me.__conf = dict()
        me.__shownames = dict()

    def __preproc_item(me, engine_name:str, item:protocol.BOTH.ConfItem) -> protocol.BOTH.ConfItem:
        conf_path = ("econf", engine_name, item.id())
        saved_value = conf.get_str(*conf_path)
        if len(saved_value) > 0:
            item.value_to(saved_value)
        else:
            conf.set_str(str(item.value()), *conf_path)
        return item

    def add(me, engine_name:str, item:protocol.BOTH.ConfItem, show_name:str) -> Self:
        if not (engine_name in me.__conf):
            me.__conf[engine_name] = dict()
        me.__conf[engine_name][item.id()] = me.__preproc_item(engine_name,item)
        if not (engine_name in me.__shownames):
            me.__shownames[engine_name] = dict()
        me.__shownames[engine_name][item.id()] = show_name
        return me

    def items(me, engine_name:str) -> List[protocol.BOTH.ConfItem]:
        if engine_name in me.__conf:
            return me.__conf[engine_name].values()
        else:
            return []

class GameManager(protocol.Receiver):
    @classmethod
    def default_conf_path(_) -> tuple: return ("game",)
    @classmethod
    def default_conf(_) -> dict:
        return {
            "rule-uid": "gomoku",
            "second-all": 600,
            "second-step": 60,
        }

    def __init__(me, ux:ux.Root) -> None:
        logger.info("GameManager", "init")
        super().__init__()
        me.__ux = ux
        me.__protocol_sys = None
        rule = game.Rules.by_uid(conf.get_str("game","rule-uid"))
        me.__game = game.Game(rule)
        me.__second_step = conf.get_int("game", "second-step")
        me.__second_all = conf.get_int("game", "second-all")
        me.__default_engine_id = Player.DEFAULT_ENGINE_ID
        me.__ai_in_think = False
        me.__players = [Player(game.P_BLACK, me.__second_step, me.__second_all),
                        Player(game.P_WHITE, me.__second_step, me.__second_all)]
        me.__player_now = 0
        me.__engine_conf = EngineConf()
        me.__second_clock = datetime.now()
        me.__io_queue = Queue(10)
        logger.info("GameManager", "has init")

    def raii(me) -> None:
        logger.info("GameManager", "raii")
        logger.info("GameManager", "has raii")

    def reset(me, /, rule_uid:str=None, second_step:int=0, second_all:int=0) -> Self:
        if isinstance(rule_uid,str):
            rule = game.Rules.by_uid(rule_uid)
            if isinstance(rule, game.RuleBase):
                me.__game.change_rule(rule)
        me.__second_step = second_step
        me.__second_all = second_all
        me.__players[0].enable_ai(False).set_seconds(second_step, second_all)
        me.__players[1].enable_ai(False).set_seconds(second_step, second_all)
        me.__player_now = 0
        me.__second_clock = datetime.now()
        if (me.__game.next_step() > 1) or me.__ai_in_think:
            me.__ai_in_think = False
            me.__game.reset()
            me.__protocol_sys.send_alive_connects(protocol.Encode.SetBoard(me.__game.board(), None))
        return me

    def now_game(me) -> game.Game: return me.__game

    def second_all(me) -> int: return me.__second_all

    def second_step(me) -> int: return me.__second_step

    def set_protocol_sys(me, protocol_sys:protocol.System) -> None:
        me.__protocol_sys = protocol_sys

    def ai_in_think(me) -> bool: return me.__ai_in_think

    def set_default_ai(me, engine_id:int) -> Self:
        if me.__players[0].engine_id() == me.__default_engine_id:
            me.__players[0].set_engine(engine_id)
        if me.__players[1].engine_id() == me.__default_engine_id:
            me.__players[1].set_engine(engine_id)
        me.__default_engine_id = engine_id
        return me

    def has_default_ai(me) -> bool:
        return (me.__default_engine_id > Player.DEFAULT_ENGINE_ID)

    def sync_gameboard_to_ai(me) -> Self:
        now_board = me.__game.board()
        lastmove = me.__game.last()
        me.__protocol_sys.send_alive_connects(protocol.Encode.SetBoard(now_board, lastmove))
        return me

    def let_ai_move_step(me) -> Self:
        if not (me.__ai_in_think or me.__game.has_over()):
            engine_id = me.__default_engine_id
            if me.now_player().use_ai() and (me.now_player().engine_id() > Player.DEFAULT_ENGINE_ID):
                player_engine_id = me.now_player().engine_id()
                if me.__protocol_sys.connect_usable(player_engine_id):
                    engine_id = player_engine_id
                else:
                    me.now_player().set_engine(engine_id)
            if me.__protocol_sys.connect_usable(engine_id):
                me.__ai_in_think = True
                next_piece = me.__game.next_piece()
                second_usable = me.__second_step
                me.__protocol_sys.send_connect(engine_id, protocol.Encode.Get(next_piece, second_usable, str(engine_id)))
        return me

    def __toggle_player(me) -> Self:
        me.__players[me.__player_now].set_seconds(me.__second_step, me.__players[me.__player_now].second_all())
        me.__player_now = 1 if (me.__player_now == 0) else 0
        me.__second_clock = datetime.now()
        return me

    def player_black(me) -> Player:
        return me.__players[0]

    def player_white(me) -> Player:
        return me.__players[1]

    def next_player(me) -> Player:
        return me.__players[1 if (me.__player_now == 0) else 0]

    def now_player(me) -> Player:
        return me.__players[me.__player_now]

    def now_player_move(me, h:int, v:int):
        if not me.__ai_in_think:
            if me.__game.canmove(h,v):
                me.__game.move(h,v)
                me.sync_gameboard_to_ai()
                me.__toggle_player()
        return me

    def ai_move(me, h:int, v:int, id:str) -> Self:
        if me.__ai_in_think:
            me.__ai_in_think = False
            engine_id = int(id)
            engine_name = me.__protocol_sys.connect_at(engine_id).name()
            if me.__game.canmove(h,v):
                piece_name = me.now_player().name()
                loctext = protocol.Const.LOC_TO_TEXT[game.hv_to_loc(h,v)]
                me.__game.move(h,v)
                me.__ux.send("log/push", lang.path("game","ai-move-loc", PIECE=piece_name, ID=engine_id, NAME=engine_name, LOC=loctext))
                me.sync_gameboard_to_ai()
            else:
                me.__ux.send("log/push", lang.path("err","ai-move-loc-invalid", ID=engine_id, NAME=engine_name))
                me.__protocol_sys.disconnect(engine_id)
                if me.now_player().engine_id() == engine_id:
                    me.now_player().engine_id(False)
            me.__toggle_player()
        return me

    def undo(me) -> Self:
        if not me.__game.has_over():
            if me.__ai_in_think:
                piece_name = me.now_player().name()
                me.__ux.send("log/push", lang.path("warn", "ai-in-think-ban-change", PIECE=piece_name))
            else:
                step = me.__game.next_step()
                if me.__game.undo().next_step() != step:
                    me.__players[0].enable_ai(False)
                    me.__players[1].enable_ai(False)
                    me.__toggle_player().sync_gameboard_to_ai()
        return me

    def redo(me) -> Self:
        if not me.__game.has_over():
            if me.__ai_in_think:
                piece_name = me.now_player().name()
                me.__ux.send("log/push", lang.path("warn", "ai-in-think-ban-change", PIECE=piece_name))
            else:
                step = me.__game.next_step()
                if me.__game.redo().next_step() != step:
                    me.__players[0].enable_ai(False)
                    me.__players[1].enable_ai(False)
                    me.__toggle_player().sync_gameboard_to_ai()
        return me

    def engine_conf(me) -> EngineConf: return me.__engine_conf

    def on_frame(me) -> None:
        if (me.__protocol_sys.n_alive_connect() > 0):
            if not me.__protocol_sys.connect_usable(me.__default_engine_id):
                for id in range(me.__protocol_sys.n_connect()):
                    if me.__protocol_sys.connect_usable(id):
                        me.__default_engine_id = id
                        break
            if not me.__game.has_over():
                if (datetime.now() - me.__second_clock).total_seconds() >= 1.0:
                    me.__second_clock = datetime.now()
                    if me.now_player().on_second().has_overtime():
                        me.__game.now_piece_overtime()
                elif not me.__io_queue.empty():
                    io = me.__io_queue.get()
                    if callable(io):
                        io()
                elif me.now_player().use_ai():
                    me.let_ai_move_step()
        else:
            me.__ai_in_think = False
            me.__players[0].enable_ai(False)
            me.__players[1].enable_ai(False)

    ##[override protocol.Receiver]
    def Move(me, conn:protocol.Connection, id:str, result:protocol.EU.Move) -> None:
        if result.is_point():
            point = result.point()
            io = (lambda:me.ai_move(point.h(), point.v(), id))
            me.__io_queue.put(io)
        elif result.is_swap:
            io = (lambda:me.now_player_swap())
            me.__io_queue.put(io)
        elif result.is_pass:
            io = (lambda:me.now_player_pass())
            me.__io_queue.put(io)

    ##[override protocol.Receiver]
    def Status(me, conn:protocol.Connection, slt:str) -> None:
        me.__ux.send("gamestatus/aistatus", lang.path("gamestatus","aistatus-in-status", ID=conn.id(), TEXT=slt))

    ##[override protocol.Receiver]
    def Say(me, conn:protocol.Connection, mlt:str) -> None:
        me.__ux.send("log/push", lang.path("game","ai-say", ID=conn.id(), SAY=mlt))

    ##[override protocol.Receiver]
    def Conf(me, conn:protocol.Connection, item:protocol.BOTH.ConfItem, show_name:str) -> None:
        io_a = (lambda *_:me.engine_conf().add(conn.name(), item, show_name))
        io_b = (lambda *_:me.__ux.overtime(f"engine-conf/{conn.name()}"))
        me.__io_queue.put(R.pipe(io_a, io_b))

    ##[override protocol.Receiver]
    def _Other(me, conn:protocol.Connection, raw:str) -> None:
        logger.warnning("EngineReceiver", f"Take a Unkonwn Message from '{conn.name()}': {raw}")

    ##[override protocol.Receiver]
    def protocol_on_connected(me, conn:protocol.Connection) -> None:
        if not me.has_default_ai():
            me.set_default_ai(conn.id())
        me.__ux.send("log/push", lang.path("info","engine-has-ready", ID=conn.id(), NAME=conn.name()))

    ##[override protocol.Receiver]
    def protocol_on_connect_fail(me, engine_path:Path) -> None:
        me.__ux.send("log/push", lang.path("err", "engine-not-ready", PATH=engine_path))

    ##[override protocol.Receiver]
    def protocol_on_connect_not_usable(me, conn:protocol.Connection) -> None:
        me.__ux.send("log/push", lang.path("err", "engine-not-useable", PATH=conn.path()))

class UxIO(ux.IO):
    def __init__(me, gm:GameManager, ux:ux.Root, protocol_sys:protocol.System) -> None:
        me.__gm = gm
        me.__ux = ux
        me.__protocol_sys = protocol_sys
        me.__game_uptime = me.__gm.now_game().updatetime()

    def __log_push_new_game(me) -> None:
        rule_name = lang.path("game","rule", me.__gm.now_game().rule().uid())
        second_all = lang.time_by_second(me.__gm.second_all())
        second_step = lang.time_by_second(me.__gm.second_step())
        me.__ux.send("log/push", lang.path("game","newgame", RULE=rule_name, TIME_ALL=second_all, TIME_STEP=second_step))

    def now_game(me) -> game.Game:
        return me.__gm.now_game()

    def black_seconds(me) -> Tuple[int, int]:
        return (me.__gm.player_black().second_step(), me.__gm.player_black().second_all())

    def white_seconds(me) -> Tuple[int, int]:
        return (me.__gm.player_white().second_step(), me.__gm.player_white().second_all())

    def black_use_ai(me) -> bool:
        return me.__gm.player_black().use_ai()

    def white_use_ai(me) -> bool:
        return me.__gm.player_white().use_ai()

    def black_use_ai_set(me, use_ai: bool) -> None:
        me.__gm.player_black().enable_ai(use_ai)

    def white_use_ai_set(me, use_ai: bool) -> None:
        me.__gm.player_white().enable_ai(use_ai)

    def now_in_think(me) -> bool:
        return me.__gm.ai_in_think()

    def now_use_ai(me) -> bool:
        return me.__gm.now_player().use_ai()

    def now_engine_id(me) -> int:
        return me.__gm.now_player().engine_id()

    def now_name(me) -> str:
        return me.__gm.now_player().name()

    def next_use_ai(me) -> bool:
        return me.__gm.next_player().use_ai()

    def next_engine_id(me) -> int:
        return me.__gm.next_player().engine_id()

    def next_name(me) -> str:
        return me.__gm.next_player().name()

    def engine_conf(me, name:str) -> List[protocol.BOTH.ConfItem]:
        return me.__gm.engine_conf().items(name)

    def n_engine(me) -> int:
        return me.__protocol_sys.n_connect()

    def n_engine_usable(me) -> int:
        return me.__protocol_sys.n_alive_connect()

    def engine_usable_connents(me) -> List[protocol.Connection]:
        conn_ids = R.filter(me.__protocol_sys.connect_usable, [id for id in range(me.__protocol_sys.n_connect())])
        return R.map(me.__protocol_sys.connect_at, conn_ids)

    def on_command_newgame(me, rule_uid:str, second_step:int, second_all:int) -> None:
        me.__gm.reset(rule_uid, second_step, second_all)
        me.__ux.send("log/clear")
        me.__log_push_new_game()
        if me.__protocol_sys.n_alive_connect() < 1:
            me.__ux.send("log/push", lang.path("warn", "no-useable-engine"))

    def on_command_reset(me) -> None:
        me.on_command_newgame(rule_uid=None, second_step=me.__gm.second_step(), second_all=me.__gm.second_all())

    def on_command_ai_step(me) -> None:
        me.__gm.let_ai_move_step()

    def on_command_undo(me) -> None:
        me.__gm.undo()

    def on_command_redo(me) -> None:
        me.__gm.redo()

    def on_board_move(me, h: int, v: int) -> None:
        me.__gm.now_player_move(h, v)

    def on_ready(me) -> None:
        me.__ux.send("log/push", lang.path("info","ux-has-ready"))
        me.__ux.send("log/push", lang.path("info","begin-conn-engine"))

        rspath = Const.ENGINE_DIR / Const.RS_PATH
        if rspath.is_file():
            me.__protocol_sys.connect(rspath)
        else:
            me.__ux.send("log/push", lang.path("err","rs-not-fount", PATH=rspath))

        me.__log_push_new_game()
        me.__ux.overtime("gameboard")

    def on_frame(me) -> None:
        me.__gm.on_frame()
        game_uptime = me.__gm.now_game().updatetime()
        if me.__game_uptime != game_uptime:
            me.__game_uptime = game_uptime
            me.__ux.overtime("gameboard")

class RAII:
    _objs = list()
def raii_add(obj_can_raii):
    RAII._objs.insert(0, obj_can_raii)
    return obj_can_raii
def raii():
    print("[toplevel rail]")
    for obj in RAII._objs:
        obj.raii()
    RAII._objs = None
    conf._raii_by_main_use_only()
    logger._raii_by_main_use_only()
    print("[toplevel has rail]")

def load_conf():
    logger.info("main.load_conf", "set defaults")
    conf.add_default(GameManager.default_conf(), *(GameManager.default_conf_path()))
    logger.info("main.load_conf", "load from storage")
    logger.info("main.load_conf", "done")

def main():
    print("[toplevel init]")
    logger._init_by_main_use_only()
    if lang.init_by_main_use_only(Const.DEFAULT_LANG_ID):
        conf._init_by_main_use_only()
        load_conf()
        game.Rules._init_by_main_use_only()
        game_ux = raii_add(ux.make_root(Const.FPS))
        game_manager = raii_add(GameManager(game_ux))
        protocol_sys = raii_add(protocol.System(game_manager))
        game_manager.set_protocol_sys(protocol_sys)
        game_ux.start(UxIO(game_manager, game_ux, protocol_sys))

    else:
        logger.error("main", f"lang init fail by default({Const.DEFAULT_LANG_ID})")
        ux.popup_error(message="因为语言包缺失或损坏，无法启动界面，请重新下载本程序\nbecause lack language-pack, can't start gui, please re-download program.")
try:
    from os import chdir
    # RESET WORK DIRECTORY
    if Path('.').resolve().name.lower() == "support":
        chdir("../")
    main()
except:
    logger.write_traceback_now()
    print_exc()
finally:
    raii()
